package com.gotobject.junit.internal.runners.model;

import com.gotobject.junit.If;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.junit.runners.model.TestClass;
import org.junit.internal.runners.model.ReflectiveCallable;

/**
 * contains the necessary elements to execute either an instance method or
 * a static one.
 *
 * @author Huascar A. Sanchez (huascar.sanchez@gmail.com)
 */
public class ConditionInvocation {
    private final Class<?>  container;
    private final Method    methodToCall;
    private final Object    containerInstance;
    private final boolean   expectedOutcome;

	public static final ConditionInvocation NULL_ONE = new ConditionInvocation(null, null, null, false);

	ConditionInvocation(
		Class<?> container,
		Method methodToCall,
		Object containerInstance,
		boolean expectedOutcome
	){
        this.container          = container;
        this.methodToCall       = methodToCall;
        this.containerInstance  = containerInstance;
        this.expectedOutcome    = expectedOutcome;
    }

    public static ConditionInvocation of(
		Class<?> container,
		Method methodToCall,
		Object containerInstance,
		boolean expectedOutcome
	){
        return new ConditionInvocation(container, methodToCall, containerInstance, expectedOutcome);
    }

    public static ConditionInvocation of(If annotation, TestClass andTestClass) throws Throwable {
        final String 	 condition 			= annotation.condition();
        final Class<?> 	 conditionCallee 	= annotation.callee();
        final boolean    expectedOutcome    = annotation.is();

        final ConditionInvocation provider;

        if(If.Nobody.class.equals(conditionCallee)){
            final Method methodToCall = methodFor(andTestClass.getJavaClass().getDeclaredMethods(), condition);
            final boolean isStatic 	  = Modifier.isStatic(methodToCall.getModifiers());

            provider = of(
                andTestClass.getJavaClass(),
                methodToCall,
                isStatic ? null : andTestClass.getOnlyConstructor().newInstance(),
                expectedOutcome
            );
        } else {
            final Method  methodToCall = methodFor(conditionCallee.getDeclaredMethods(), condition);
            final boolean isStatic 	   = Modifier.isStatic(methodToCall.getModifiers());
            final Object  instance 	   = isStatic ? null : new ReflectiveCallable(){
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return makeSourceOf(conditionCallee);
                }
            }.run();

           provider = of(conditionCallee, methodToCall, instance, expectedOutcome);
        }

        return provider;
    }


    public Class<?> getContainer() {
        return container;
    }


    public Method getMethodToCall() {
        return methodToCall;
    }


    public Object getContainerInstance() {
        return containerInstance;
    }


    public boolean getExpectedOutcome() {
        return expectedOutcome;
    }


    public Object invoke() throws Exception {
        return getMethodToCall().invoke(getContainerInstance());
    }

	/**
     * make a new object source.
     * @param klass type.
     * @return Object source where a condition should be invoked.
     * @throws Exception if the source cannot be created.
     */
    private static Object makeSourceOf(Class<?> klass) throws Exception {
		return new TestClass(klass).getOnlyConstructor().newInstance();
	}

    /**
     * indicates which method matches or contains a given condition.
     * @param methods to be searched.
     * @param condition to be matched.
     * @return Method matching a given condition.
     */
    private static Method methodFor(Method[] methods, String condition){
		Method result = null;
		for(Method eachMethod : methods){
			if(eachMethod.getName().equals(condition)){
				result = eachMethod;
			}
		}
		return result;
	}

    @Override
    public String toString() {
        return getContainer().getName() + "#" + getMethodToCall().getName();
    }
}
